为什么说 Module Federation 天生是模块级的微前端?
Module Federation 不是 webpack 5 的特性么?它和微前端有毛线关系?
不着急解释,我们先写个 Module Federation 的 demo 再说。
用 create-react-app 创建一个 react + webpack 的项目:
yarn create react-app aaa
然后进入项目目录执行
git init
git add .
git commit -m 'first commit'
之后执行
yarn run eject
默认 create-react-app 创建的项目不会暴露 react 配置文件,需要执行 eject 才会出来,但那之前需要把本地工作区的文件保存到 git 仓库才行。
这样就创建了一个 react + webpack 并且 webpack 配置文件可以修改的项目:
webpack 配置文件在这里:
同样的方式,再创建一个 bbb 项目。
现在我们就来通过 Module Federation 让 aaa 里的一个 Button 在 bbb 里面使用。
我们在 aaa 里加一个这样的 Button:
import './button.css';
export default function Button() {
return <button className="guang">guang</button>
}
.guang {
padding: 5px;
color: #fff;
background: blue;
border-radius: 5px;
border: 1px solid #fff;
}
yarn start 跑起开发服务,就可以看到它渲染出来是这样的:
我们想通过 Module Federation 把这个 Button 暴露出去该怎么做呢?
这样:
修改 webpack 配置文件,在 plugins 里添加这个插件:
const { ModuleFederationPlugin } = require('webpack').container;
new ModuleFederationPlugin({
name: 'aaa_app',
filename: 'aaaEntry.js',
exposes: {
'./Button': './src/Button.jsx',
}
})
它的含义就是创建一个 name 为 aaa_app 的共享包。
这个共享包 exposes 暴露出了 Button 这个共享模块。
它对应的文件名是 aaaEntry.js。
然后重新 yarn start 跑下开发服务器,刷新页面,你就会看到页面请求了 aaaEntry.js 这个文件:
里面声明了一个 aaa_app 的变量。
这就是说 webpack 把这个组件的代码分离到了这个文件里。
这样别的 webpack 应用就可以直接用这个组件了。
不信我们来试试看:
bbb 项目里同样修改 webpack 配置,引入这个插件:
const { ModuleFederationPlugin } = require('webpack').container;
new ModuleFederationPlugin({
remotes: {
'aaa-app': 'aaa_app@http://localhost:3001/aaaEntry.js',
}
})
引入的时候使用 remotes 注册,这段配置就是注册了一个运行时的 Module,名字叫 aaa-app,它的值来自 http://localhost:3001/aaaEntry.js 这个文件里的 aaa_app 变量。
这样代码里就可以引入来用了:
因为是异步组件,所以用 React.lazy 包裹,具体取这个组件的逻辑就是用 webpack 提供的 import() 来异步加载模块。
这里通过 aaa-app/Button ,也就是共享包名/模块名的方式来引入。
跑下看看:
报错了,为什么呢?
看下网络,aaaEntry.js 这个文件已经被加载了呀:
只要取它的 aaa_app 变量不就行了么?
哪里有问题?
仔细看下你会发现,这个入口是 localhost:3001 下的文件没错:
但是它里面引入的这个 Button 却是 localhost:3002 下的:
那自然找不到了!
这是因为这时候 publicPath 不对:
所以去改下 aaa 的 ouput.publicPath 的配置,固定为 http://localhost:3001/
然后重新 yarn start,刷新下页面:
这时 publicPath 就对了,Button 的代码也就正常加载了:
这个 http://localhost:3002 就是 bbb 应用,可以看到它加载了 aaa 应用的 Button,渲染了出来。
而 http://localhost:3001 的 aaa 应用自然也有这个 Button:
这就是所谓的模块联邦 Module Federation !
感受到它能干什么了么?
Module Federation 能够把一个应用的一些模块导出,供别的应用异步引入这些模块。
方式就是一个应用通过 ModeleFederationPlugin 声明 exposes 的模块名字和路径,另一个应用通过 remotes 声明用到的一些模块来自于哪个文件的哪个变量。
这样当用到这个模块的时候,就回去异步请求对应的 url,取出其中的变量值。
这里要特别注意导出模块的应用需要固定 publicPath,不然加载文件的路径会有问题。
我们完成了一个应用导出 Button 在另一个应用里用,这个是单向的。
那反过来可不可以呢?
自然也是可以的,我们来试一下:
在 bbb 里添加这样一个 button:
import './button.css';
export default function Button() {
return <button className="dong">dongdongdong</button>
}
.dong {
padding: 5px;
color: #fff;
background: green;
border-radius: 5px;
border: 1px solid #fff;
}
渲染出来是这样的:
然后用 ModuleFederationPlugin 声明下 exposes 来导出它:
const { ModuleFederationPlugin } = require('webpack').container;
new ModuleFederationPlugin({
name: 'bbb_app',
filename: 'bbbEntry.js',
exposes: {
'./Button': './src/Button.jsx'
},
remotes: {
'aaa-app': 'aaa_app@http://localhost:3001/aaaEntry.js',
}
})
在之前的基础上,同样加上 name(变量名)、fileName(文件名)、exposes(导出的模块)的配置。
然后配置 output.publicPath 固定为 http://localhost:3002/
然后去 aaa 应用里也改一下 ModuleFederationPlugin 的配置,引入这个 bbb 的导出:
const { ModuleFederationPlugin } = require('webpack').container;
new ModuleFederationPlugin({
name: 'aaa_app',
filename: 'aaaEntry.js',
exposes: {
'./Button': './src/Button.jsx',
},
remotes: {
'bbb-app': 'bbb_app@http://localhost:3002/bbbEntry.js',
}
})
在组件里引入下:
重新跑一下 aaa 和 bbb 的 dev server。
然后刷新页面:
这时候你就可以看到 aaa 里面渲染了 bbb 的组件,bbb 里渲染了 aaa 的组件。
两个应用实现了模块的双向使用!
除了导出业务模块外,库的模块也可以共用,比如 react、react-dom 这种库。
两个应用都这样声明:
重新 yarn start 跑开发服务,然后刷新页面。
但这时会报错:
因为现在 react 和 react-dom 被分了出来,变成异步加载的模块了。
而你在入口里同步使用了异步模块导出 api 是不行的。
要改造一下,把原来的 index.js 改名为 bootstrap.js。
然后在新的 index.js 里异步 import 这个 bootstrap.js 就好了:
因为整个应用都变成了异步的,之前单独引入的异步组件的写法也就可以简化了:
这时就能正常渲染了:
而且 react、react-dom 这俩包,还有用到的 button 组件,都是异步加载的:
这些都是可以在 aaa、bbb 之间共享的模块,也就是模块联邦的模块!
这就是 Module Federation。
那回过头来看看最开始那个问题,为什么说 Module Federation 天生是模块级的微前端呢?
因为微前端做的事情不就是一个应用里复用另一个应用的代码么?
只不过一般是整个应用的共享,比如 qiankun 是在主应用里注册微应用的地址:
并且在微应用里写一下加载、卸载等生命周期的函数:
这样就可以在路由切换的时候自动加载微应用并执行它的生命周期函数来激活它。
微前端也就是多了一个异步加载应用的逻辑。
而 Module Federation 呢?
是在一个应用里注册另一个应用的模块代码:
当用到这些模块的时候,webpack 会异步加载对应的 chunk,拿到对应变量的值,也就是共享的模块。
这个和应用级别的微前端不是一样的流程么?
都是异步加载并执行代码。
只不过一个是应用级别,一个是模块级别。
所以说,模块联邦是天生的模块级的微前端。
总结
Module Federation 是 webpack5 提供的用于应用之间共享模块的机制,只要用 ModuleFederationPlugin 声明 exposes 的模块,另一个应用里用 ModuleFederationPlugin 声明 remotes 导入的模块,就可以直接用别的应用的模块了。
这就是它为什么会叫模块联邦。
除了业务模块外,库模块也可以共享。只不过要注意这些模块都是异步加载的,所以要用 import()来异步引入。
单独引入异步组件需要用 React.lazy(() => import('xx/yy')) 的形式,或者把整个应用用 import() 来异步加载。
此外,还要注意要固定 output.publicPath,不然引入模块的时候路径会有问题。
Module Federation 是天生的模块级微前端,它和 qiankun 一样,都是用到另一个应用的代码时,异步去某个地址下载它的代码,然后跑起来,只不过一个是应用级,一个是模块级。